package model.management;

import gui.product.ProductData;

import java.util.*;

import model.entity.INode;
import model.entity.Item;
import model.entity.Product;
import model.entity.container.ProductContainer;
import model.entity.container.StorageUnit;
import model.events.EntityEvent;
import model.events.EntityEventEmitter;
import model.events.EntityModifiedEvent;
import persistence.PersistenceMode;
import persistence.ProductsDAO;
import reports.visitor.IVisitor;

import common.Result;
import common.ValueResult;

/**
 * Track the instance of the list of product manager
 * 
 * @author Ian Ryu, Paula Chen
 * 
 *         {@invariant products.size() >= 0}
 * 
 */
//@formatter:off
public class ProductManager extends EntityEventEmitter implements INode
{
  static ProductManager INSTANCE          = new ProductManager();
  
  public static ProductManager getInstance()
  {
	  return INSTANCE;
  }

  private Set<Product>          products          = new HashSet<Product>();
  
  private Map<String, Product>  productsByBarcode = new HashMap<String, Product>();
  
  private Map<ProductContainer, Set<Product>> productsByContainer
                            = new HashMap<ProductContainer, Set<Product>>();
 

  private ProductManager()
  {
    load();
  }

  /**
   * Validates whether the given product can be added to product.
   * 
   * @param product
   *          The product to validate.
   * 
   * @return a result, whose status indicates whether the given product can be added to this
   *         container.
   * 
   *         {@pre product != null}
   * 
   */
  public static Result validateAddProduct(Product product)
  {
    return INSTANCE._validateAddProduct(product);
  }

  public static ValueResult<EntityEvent> addProduct(Product product)
  {
    return INSTANCE._addProduct(product);
  }
  
  
  /**
   * Validates whether the given product can be added to this product container. If so, the product
   * is added to this container.
   * 
   * @param product
   *          The product to be added to this container.
   * 
   * @return a result, indicating whether this operation was successful or not. {@pre product !=
   *         null} {@pre products.size() = (old)products.size() + 1}
   * 
   */
  public static Result validateAddProductToContainer(ProductContainer container, Product product)
  {
    return INSTANCE._validateAddProductToContainer(container, product);
  }
  
  public static Result addProductToContainer(ProductContainer container, Product product)
  {
    return INSTANCE._addProductToContainer(container, product);
  }
  

  /**
   * Validates whether the given product can be removed from the given container.
   * 
   * @param product
   *          The product to validate removal from the given container.
   * @param container
   *          The container to remove the product from
   * 
   * @return a result, whose status indicates whether the given product can be removed from this
   *         container.
   * 
   *         {@pre product != null}
   * 
   */
  public static Result validateRemoveProductFromContainer(Product product,
    ProductContainer container)
  {
	  return INSTANCE._validateRemoveProductFromContainer(product, container);
  }
  
  public static Result removeProductFromContainer(Product product, ProductContainer container)
  {
	  return INSTANCE._removeProductFromContainer(product, container);
  }

  /**
   * Validates whether the given product can be removed from this product container. If so, the
   * product is removed from this container.
   * 
   * @param product
   *          The product to be removed from this container.
   * 
   * @return a result, indicating whether this operation was successful or not.
   * 
   *         {@pre product != null} {@pre products.size() = (old)products.size() - 1}
   * 
   */
  public static Result validateRemoveProduct(Product product)
  {
	  return INSTANCE._validateRemoveProduct(product);
  }
  
  public static Result removeProduct(Product product)
  {
    return INSTANCE._removeProduct(product);
  }

  /**
   * Validates whether the given product can be updated.
   * 
   * @param product
   *          The product to validate.
   * 
   * @return a result, whose status indicates whether the given product can be updated.
   * 
   *         {@pre product != null}
   * 
   */
  
  public static Result validateUpdateProduct(Product before, Product after)
  {
	  return INSTANCE._validateUpdateProduct(before, after);
  }
  
  public static Result updateProduct(Product before, Product after)
  {
	  return INSTANCE._updateProduct(before, after);
  }

  /**
   * Validates whether the given product can be updated this product container. If so, the product
   * is updated.
   * 
   * @param product
   *          The product to be updated this container.
   * 
   * @return a result, indicating whether this operation was successful or not.
   * 
   *         {@pre product != null} {@post products.size == (old)products.size()}
   * 
   */
  public static Result validateEditProduct(Product before, ProductData after)
  {
	  return INSTANCE._validateEditProduct(before, after);
  }
  
  public static Result editProduct(Product target, ProductData data)
  {
	  return INSTANCE._editProduct(target, data);
  }
  
  /**
   * @return An iterator of all <code>Product</code>s in the system.
   */
  public static Iterator<Product> getAllProducts()
  {
    return Collections.unmodifiableSet(INSTANCE.products).iterator();
  }

  /**
   * Clears all Product data.
   */
  public static void clear()
  {
    INSTANCE.products.clear();
  }

  /**
   * Gets the product from its global barcode
   * 
   * @param code
   *          The barcode of the product
   */
  public static Product getProduct(String code)
  {
    return INSTANCE.productsByBarcode.get(code);
  }

  public static void itemAdded(Item item)
  {
    INSTANCE._itemAdded(item);
  }

  public static Collection<ProductData> getProductData(ProductContainer container)
  {
    return INSTANCE._getProductData(container);
  }

  public static Result moveProductToContainer(ProductContainer container, Product product,
      Iterator<Item> items)
  {
    return INSTANCE._moveProductToContainer(container, product, items);
  }

  private Result _validateAddProduct(Product product)
  {
    if (product == null)
      return new Result(false, "Cannot add product - Null product.");
    Result validProduct = product.validateProduct();
    if (!validProduct.getStatus())
      return validProduct;
    if (products.contains(product))
      return new Result(false, "Cannot add product - Product already exists.");

    return new Result(true);
  }

  private ValueResult<EntityEvent> _addProduct(Product product)
  {
    Result result = _validateAddProduct(product);
    if (!result.getStatus())
    {
      ValueResult<EntityEvent> r = new ValueResult<EntityEvent>();
      r.setStatus(false);
      r.setMessage(result.getMessage());
    }
    
    index(product);
    
    save(); //for serialize mode; do nothing in sql mode
    PersistenceMode.getSystemMode().getProductsDAO().add(product);

    EntityEvent event = generateProductAddedEvent(product);
    fireAddedEvent(event);

    return new ValueResult<EntityEvent>(true, "", event);
  }
  
  private Result _validateAddProductToContainer(ProductContainer container, Product product)
  {
	  if (product == null)
		  return new Result(false, "Null product.");

	  if (!INSTANCE.products.contains(product))
		  return new Result(false, "Product doesn't exist.");

	  if (container.containsProduct(product))
		  return new Result(true, "Container already contains product");

	  return new Result(true);
  }

  private Result _addProductToContainer(ProductContainer container, Product product)
  {
    StorageUnit unit = StorageUnitManager.getStorageUnit(container);
    ProductContainer oldContainer = unit.getProductContainer(product);

    Result result;
    if (oldContainer == null) // the product is not found in the storage unit
    {
      container.addProduct(product);
      index(product, container);

      EntityEvent event = generateProductAddedEvent(product, container);
      fireAddedEvent(event);

      result = new Result(true);
    }
    else // the product is in the storage unit already
    {
      Iterator<Item> items = ItemManager.getItems(oldContainer, product);

      oldContainer.removeProduct(product);
      productsByContainer.get(oldContainer).remove(product);

      result = _moveProductToContainer(container, product, items);
    }

    return result;
  }
  
  private Result _validateRemoveProductFromContainer(Product product, ProductContainer container)
  {
    if (product == null)
    	return new Result(false, "Null product.");

	if (!INSTANCE.products.contains(product))
		return new Result(false, "Product doesn't exist.");
	
	if (!container.containsProduct(product))
	    return new Result(true, "Container does not contain product");
	
	Iterator<Item> itr = ItemManager.getItems(container, product);
	if (itr.hasNext())
	    return new Result(false, "product contains items");
	
	return new Result(true);
  }
  
  private Result _removeProductFromContainer(Product product, ProductContainer productContainer)
  {
    Result result = _validateRemoveProductFromContainer(product, productContainer);
    if (result.getStatus() == false)
      return result;

    Product gotProduct = getProduct(product.getBarcode().getCode());
    if (productContainer == null)
    {
      unIndex(gotProduct, gotProduct.getContainers());
    }
    else
    {
      product.removeFromProductContainer(productContainer);
      productsByContainer.get(productContainer).remove(product);
      productContainer.removeProduct(product);
    }

    EntityEvent event = generateProductRemovedEvent(product, productContainer);
    fireRemovedEvent(event);

    return result;
  }
  
  private Result _validateRemoveProduct(Product product)
  {
	  if(product == null)
	  {
		  return new Result(false, "Error: product is null");
	  }
	  if(!products.contains(product))
	  {
		  return new Result(false, "Error: product in not in system");
	  }
	  
	  return new Result(true);
  }
  
  private Result _removeProduct(Product product)
  {
	  Result result = _validateRemoveProduct(product);
	  if(result.getStatus() == false)
	  {
		  return result;
	  }
	  else
	  {
		  products.remove(product);
		  
		  EntityEvent event = generateProductRemovedEvent(product);
		  fireRemovedEvent(event);
		  
		  return new Result(true);
	  }
  }

  private void _itemAdded(Item item)
  {
    Product product = item.getProduct();
    ProductContainer container = item.getContainer();
    container.addProduct(product);
    index(product, container);
  }

  private EntityEvent generateProductAddedEvent(Product product, ProductContainer container)
  {
    return new EntityEvent(product, EntityEvent.ENTITY_ADDED, null, container);
  }

  private EntityEvent generateProductAddedEvent(Product product)
  {
    return new EntityEvent(product, EntityEvent.ENTITY_ADDED, null, null);
  }
  
  private EntityEvent generateProductRemovedEvent(Product product)
  {
    return new EntityEvent(product, EntityEvent.ENTITY_REMOVED, null, null);
  }

  private Collection<ProductData> _getProductData(ProductContainer container)
  {
    Collection<Product> products;
    if (container == null)
      products = this.products;
    else
    {
      products = productsByContainer.get(container);
      if (products == null)
        return Collections.emptySet();
    }

    Collection<ProductData> data = new ArrayList<ProductData>(products.size());
    for (Product product : products)
      data.add(product.generateProductDataObject(container));

    return data;
  }
  
 

  private Result _moveProductToContainer(ProductContainer container, Product product,
      Iterator<Item> items)
  {
    container.addProduct(product);
    index(product, container);
    return ItemManager.moveItemsToProductContainer(items, container);
  }
  
  private Result _validateUpdateProduct(Product before, Product after)
  {
	  if(before.equals(after))
		  return new Result(true, "Same Product - before and after products are same");
	  if(!products.contains(before))
		  return new Result(false, "Not exists - before insatance does not exost");
	  if(products.contains(after))
		  return new Result(false, "After instance is already exists");
	  
	  return new Result(true);		  
  }
  
  private Result _updateProduct(Product before, Product after)
  {
	  Result result = _validateUpdateProduct(before, after);
	  if(result.getStatus() == false)
	  {
		  return result;
	  }
	  else
	  {
		  products.remove(before);
		  productsByBarcode.remove(before.getBarcode().getCode());
		  productsByBarcode.remove(before.getContainers());
		  index(after);
		  
		  ProductsDAO dao = PersistenceMode.getSystemMode().getProductsDAO();
		  dao.update(after);
		  
		  return new Result(true);
	  }
	  
  }
  
  private Result _validateEditProduct(Product before, ProductData after)
  {
	  if(after.getBarcode() == null || after.getDescription() == null 
			  || after.getDescription().equals(before.getDescription()))
	  {
		  return new Result(false, "Cannot edit product - need barcode and description");
	  }
	  
	  return new Result(true);
  }
  
  private Result _editProduct(Product before, ProductData data)
  {
	  Result result = _validateEditProduct(before, data);
	  if(result.getStatus() == false)
	  {
		  return result;
	  }
	  Product after = before;
	  before = new Product(after);
	  after.setDescription(data.getDescription());
	  after.setShelfLife(Integer.parseInt(data.getShelfLife()));
	  after.setThreeMonthSupply(Integer.parseInt(data.getSupply()));

	  EntityModifiedEvent event = generateProductModifiedEvent(before, after);
	  fireModifiedEvent(event);
	  
	  return result;
  }
  
  private EntityModifiedEvent generateProductModifiedEvent(Product before, Product after)
  {
	  return new EntityModifiedEvent(before, after);
  }

  private void index(Product product)
  {
    products.add(product);
    productsByBarcode.put(product.getBarcode().getCode(), product);

    for (ProductContainer container : product.getContainers())
    {
      Set<Product> list = productsByContainer.get(container);
      if (list == null)
      {
        list = new HashSet<Product>();
        productsByContainer.put(container, list);
      }

      list.add(product);
    }
    save();
  }

  private void index(Product product, ProductContainer container)
  {
    products.add(product);
    productsByBarcode.put(product.getBarcode().getCode(), product);

    Set<Product> list = productsByContainer.get(container);
    if (list == null)
    {
      list = new HashSet<Product>();
      productsByContainer.put(container, list);
    }

    list.add(product);
    save();
  }

  private void unIndex(Product product, List<ProductContainer> containers)
  {
    products.remove(product);
    productsByBarcode.remove(product.getBarcode().getCode());

    for (ProductContainer container : containers)
    {
      productsByContainer.get(container).remove(product);
      container.removeProduct(product);
    }
  }

 

  private EntityEvent generateProductRemovedEvent(Product product,
      ProductContainer productContainer)
  {
    return new EntityEvent(product, EntityEvent.ENTITY_REMOVED, null, productContainer);
  }

  static void updateUnit(StorageUnit before, StorageUnit after)
  {
    INSTANCE._updateUnit(before, after);
  }

  private void _updateUnit(StorageUnit before, StorageUnit after)
  {
    Set<Product> set = productsByContainer.remove(before);
    productsByContainer.put(after, set);
  }

  public static void save()
  {
    ProductsDAO dao = PersistenceMode.getSystemMode().getProductsDAO();
    Result result = dao.saveProducts(INSTANCE.products);

    if (!result.getStatus())
    {
      System.err.println("Saving products failed: " + result.getMessage());
      return;
    }

    result = dao.saveProductsByContainer(INSTANCE.productsByContainer);

    if (!result.getStatus())
    {
      System.err.println("Saving products failed: " + result.getMessage());
    }
  }

  private void load()
  {
    ProductsDAO dao = PersistenceMode.getSystemMode().getProductsDAO();

    ValueResult<Set<Product>> productsResult = dao.getAll();
    if (!productsResult.getStatus())
    {
      System.err.println("Loading products failed: " + productsResult.getMessage());
      return;
    }
    products = productsResult.getValue();
    for(Product product : products){
    	if(product.getProductContainerIds() != null){
    	  for(int i = 0; i < product.getProductContainerIds().size(); i++){
    	   	int id = product.getProductContainerIds().get(i)
;    		ProductContainer pc = StorageUnitManager.getInstance().getContainer(id);
            product.addToProductContainer(pc);
            System.out.print(product.getContainers().size());
            Set<Product> list = productsByContainer.get(pc);
            if (list == null)
            {
              list = new HashSet<Product>();
              productsByContainer.put(pc, list);
            }
            list.add(product);
    	  }
    	}
    }
    productsByBarcode.clear();
    for (Product product : products)
    {
      productsByBarcode.put(product.getBarcode().getCode(), product);
    }

    ValueResult<Map<ProductContainer, Set<Product>>> productsByContainerResult = dao
        .getProductsByContainer();
    if (!productsByContainerResult.getStatus())
    {
      System.err.println("Loading products failed: " + productsByContainerResult.getMessage());
      return;
    }
  }

  private INode acceptor = null;

  /**
   * Overrides the default implementation of the INode interface with the supplied INode
   * implementation. If the supplied INode is <tt>null</tt>, the default implementation is used.
   * 
   * @param acceptor
   *          The new implementation of the INode interface for this manager to use.
   */
  public void setVisitorAcceptor(INode acceptor)
  {
    this.acceptor = acceptor;
  }

  @Override
  public void preOrderAccept(IVisitor visitor)
  {
    if (acceptor == null)
    {
      for (Product product : products)
        product.preOrderAccept(visitor);
    }
    else
      acceptor.preOrderAccept(visitor);
  }

  @Override
  public void postOrderAccept(IVisitor visitor)
  {
    if (acceptor == null)
    {
      for (Product product : products)
        product.preOrderAccept(visitor);
    }
    else
      acceptor.postOrderAccept(visitor);
  }
}